package vg.modules.opener.decoders.dot;

import com.google.common.collect.BiMap;
import com.google.common.collect.HashBiMap;
import vg.modules.opener.decoders.IDecoder;
import vg.interfaces.ll_services.data_base_service.data.header.AttributeHeader;
import vg.interfaces.ll_services.data_base_service.IGraphDataBaseService;
import vg.services.main_manager.VGMainServiceHelper;

import java.io.*;
import java.util.*;

/**
 * Dot decoder.
 *
 * @author Timur Zolotuhin (e-mail: tzolotuhin@gmail.com)
 */
public class DotDecoder implements IDecoder {
    // Key words
    private static final String STRICT = "strict";
    private static final String UNDIRECTED_GRAPH = "graph";
    private static final String DIRECTED_GRAPH = "digraph";
    private static final String GRAPH = "graph";
    private static final String NODE = "node";
    private static final String EDGE = "edge";
    private static final String SUBGRAPH = "subgraph";

    private static final String BEGIN_ATTR = "[";
    private static final String END_ATTR = "]";
    private static final String BEGIN_STMT = "{";
    private static final String END_STMT = "}";
    private static final String DELIMITER = ";";
    private static final String EQUAL = "=";
    private static final String COMMA = ",";
    private static final String DIRECTED_EDGE = "->";
    private static final String UNDIRECTED_EDGE = "--";
    private static final String COLON = ":";

    // Main data
    private DotTokenizer currDotTokenizer;
    private IGraphDataBaseService graphDataBase;

    private int graphModelId;

    private boolean currDirected;

    private Stack<BiMap<String, Integer>> vertexDotID2databaseID;
    private Stack<BiMap<String, Integer>> edgeDotID2databaseID;

    private BiMap<String, Integer> currVertexDotID2databaseID;
    private BiMap<String, Integer> currEdgeDotID2databaseID;

    @Override
    public List<Integer> decode(File file, IGraphDataBaseService graphDataBase, String graphName) throws Exception {
        if (graphDataBase == null)
            throw new IllegalArgumentException("graphDataBase == null");

        this.graphDataBase = graphDataBase;

        currDotTokenizer = null;

        graphModelId = -1;
        currDirected = false;

        currVertexDotID2databaseID = null;
        currEdgeDotID2databaseID = null;

        vertexDotID2databaseID = new Stack<BiMap<String, Integer>>();
        edgeDotID2databaseID = new Stack<BiMap<String, Integer>>();

        Reader reader = null;
        List<Integer> graphModelIds = new ArrayList<Integer>();

        try {
            reader = new FileReader(file);
            currDotTokenizer = new DotTokenizer(reader);

            // create graph model
            graphModelId = graphDataBase.createGraphModelHeader(graphName);
            graphModelIds.add(graphModelId);

            while(isNextGraph()) {
                parseGraph();
            }
        } catch (Exception ex) {
            VGMainServiceHelper.logger.printException(ex);
            throw ex;
        } finally {
            if (reader != null) {
                try {
                    reader.close();
                } catch (Exception ex) {
                    VGMainServiceHelper.logger.printException(ex);
                }
            }
        }

        return graphModelIds;
    }

    private int parseGraph() throws Exception {
        boolean strict = false;
        String id = "";
        boolean stmtListExpected = false;
        int graphId = -1;

        String token = currDotTokenizer.nextToken();

        if (token != null && token.equals(STRICT)) {
            strict = true;
            token = currDotTokenizer.nextToken();
        }

        if (token != null && token.equals(DIRECTED_GRAPH)) {
            currDirected = true;
        } else if (token != null && token.equals(UNDIRECTED_GRAPH)) {
            currDirected = false;
        } else {
            throw new Exception("[parseGraph][line number = " + currDotTokenizer.getCurrentLineNumber() + "] Unknown symbol = " + token + ". Symbol expected = " + UNDIRECTED_GRAPH + "|" + DIRECTED_GRAPH);
        }

        token = currDotTokenizer.nextToken();
        if (token != null && !token.equals(BEGIN_STMT)) {
            id = token;
            token = currDotTokenizer.nextToken();
        }

        if (token == null || !token.equals(BEGIN_STMT)) {
            throw new Exception("[parseGraph][line number = " + currDotTokenizer.getCurrentLineNumber() + "] Unknown symbol = " + token + ". Symbol expected = " + BEGIN_STMT);
        } else {
            stmtListExpected = true;
        }

        if (stmtListExpected) {
            graphId = graphDataBase.createGraphHeader(graphModelId, currDirected, true);

            if (id.length() > 0)
                graphDataBase.createGraphAttributeHeader(graphId, "name", id, AttributeHeader.STRING_ATTRIBUTE_TYPE);

            currVertexDotID2databaseID = HashBiMap.create();
            currEdgeDotID2databaseID = HashBiMap.create();

            vertexDotID2databaseID.add(currVertexDotID2databaseID);
            edgeDotID2databaseID.add(currEdgeDotID2databaseID);

            parseStmtList(graphId);

            currVertexDotID2databaseID = vertexDotID2databaseID.pop();
            currEdgeDotID2databaseID = edgeDotID2databaseID.pop();
        }

        token = currDotTokenizer.nextToken();

        if (token == null || !token.equals(END_STMT)) {
            throw new Exception("[parseGraph][line number = " + currDotTokenizer.getCurrentLineNumber() + "] Unknown symbol = " + token + ". Symbol expected = " + END_STMT);
        }

        return graphId;
    }
    private void parseStmtList(int graphId) throws Exception {
        if (currDotTokenizer.hasNextToken()) {
            parseStmt(graphId);

            String token = currDotTokenizer.peekToken();
            if (token == null) {
                throw new Exception("[parseStmtList][line number = " + currDotTokenizer.getCurrentLineNumber() + "] Unknown symbol = " + token + ". Symbol expected = " + DELIMITER);
            }

            if (token.equals(DELIMITER)) {
                currDotTokenizer.nextToken();
            }

            token = currDotTokenizer.peekToken();
            if (token != null && !token.equals(END_STMT)) {
                parseStmtList(graphId);
            }
        }
    }

    private void parseStmt(int graphId) throws Exception {
        if (currDotTokenizer.hasNextToken()) {
            String token2 = currDotTokenizer.peekToken(1);

            if (isNextAttrStmt()) {
                parseAttrStmt(graphId);
            } else if (isNextSubGraph()) {
                parseSubGraph(graphId);
            } else if (token2 != null && token2.equals(EQUAL)) {
                List<Integer> graphIds = new ArrayList<Integer>();
                graphIds.add(graphId);
                parseAttr(FOR_GRAPH, graphIds);
            } else if (isNextEdgeStmt()) {
                parseEdgeStmt(graphId);
            } else if (isNextID()){
                parseNodeStmt(graphId);
            }
        }
    }

    private String parseNodeStmt(int graphId) throws Exception {
        String id = parseNodeId();

        int vertexId;
        if (id != null && currVertexDotID2databaseID.containsKey(id)) {
            vertexId = currVertexDotID2databaseID.get(id);
        } else {
            vertexId = graphDataBase.createVertexHeader(graphId, -1);
        }

        graphDataBase.createVertexAttributeHeader(vertexId, "name", id, AttributeHeader.STRING_ATTRIBUTE_TYPE);

        currVertexDotID2databaseID.put(id, vertexId);

        if (isNextAttrList()) {
            List<Integer> vertexIds = new ArrayList<Integer>();
            vertexIds.add(vertexId);
            parseAttrList(FOR_NODE, vertexIds);
        }

        return id;
    }

    /**
     * Returns node id.
     */
    private String parseNodeId() throws Exception {
        String id = parseID();

        if (id.equals("block0008")) {
            int i = 5;
        }

        if (isNextPort()) {
            parsePort();
        }

        return id;
    }

    private void parsePort() throws Exception {
        if (isNextCompassPT()) {
            parseCompassPT();
        } else {
            String token = currDotTokenizer.nextToken();

            if (token == null || !token.equals(COLON))
                throw new Exception("[parsePort][line number = " + currDotTokenizer.getCurrentLineNumber() + "] Unknown symbol = " + token + ". Symbol expected = " + COLON);

            parseID();
            if (isNextCompassPT()) {
                parseCompassPT();
            }
        }
    }

    private void parseCompassPT () throws Exception {
        String token = currDotTokenizer.nextToken();

        if (token == null)
            throw new Exception("[parseCompassPT][line number = " + currDotTokenizer.getCurrentLineNumber() + "] Unknown symbol = " + token);

        if (token.equals(COLON))
            token = currDotTokenizer.nextToken();

        if (token == null)
            throw new Exception("[parseCompassPT][line number = " + currDotTokenizer.getCurrentLineNumber() + "] Unknown symbol = " + token);

        if (token.equals("n") || token.equals("ne") ||
                token.equals("e") || token.equals("se") ||
                token.equals("s") || token.equals("sw") ||
                token.equals("w") || token.equals("nw") ||
                token.equals("c") || token.equals("_")) {
            // TODO
            return;
        }

        throw new Exception("[parseCompassPT][line number = " + currDotTokenizer.getCurrentLineNumber() + "] Unknown symbol = " + token);
    }

    private void parseEdgeStmt(int graphId) throws Exception {
        int sourceNodeId;
        if (isNextSubGraph()) {
            sourceNodeId = parseSubGraph(graphId);
        } else {
            String strNodeId = parseNodeId();
            if (!currVertexDotID2databaseID.containsKey(strNodeId)) {
                int vertexId = graphDataBase.createVertexHeader(graphId, -1);
                currVertexDotID2databaseID.put(strNodeId, vertexId);
            }
            sourceNodeId = currVertexDotID2databaseID.get(strNodeId);
        }

        List<Integer> edgeIds = parseEdgeRHS(graphId, sourceNodeId, new ArrayList<Integer>());

        if (isNextAttrList()) {
            parseAttrList(FOR_EDGE, edgeIds);
        }
    }

    /**
     * Returns edge indexes.
     */
    private List<Integer> parseEdgeRHS(int graphId, int sourceNodeId, List<Integer> edgeIds) throws Exception {
        boolean directed = parseEdgeOP();

        int targetNodeId;
        if (isNextSubGraph()) {
            targetNodeId = parseSubGraph(graphId);
        } else {
            String strNodeId = parseNodeId();
            if (!currVertexDotID2databaseID.containsKey(strNodeId)) {
                int vertexId = graphDataBase.createVertexHeader(graphId, -1);
                currVertexDotID2databaseID.put(strNodeId, vertexId);
            }
            targetNodeId = currVertexDotID2databaseID.get(strNodeId);
        }

        // create edge in data base
        int edgeId = graphDataBase.createEdgeHeader(graphId, sourceNodeId, targetNodeId);
        edgeIds.add(edgeId);

        if (isNextEdgeRHS()) {
            parseEdgeRHS(graphId, targetNodeId, edgeIds);
        }

        return edgeIds;
    }

    private boolean parseEdgeOP() throws Exception {
        String token = currDotTokenizer.nextToken();

        if (token == null || (!token.equals(DIRECTED_EDGE) && !token.equals(UNDIRECTED_EDGE))) {
            throw new Exception("[parseEdgeOP][line number = " + currDotTokenizer.getCurrentLineNumber() + "] Unknown symbol = " + token + ". Symbol expected = " + DIRECTED_EDGE + "|" + UNDIRECTED_EDGE);
        }

        return token.equals(DIRECTED_EDGE);
    }

    // TODO Visual Graph hasn't graph attributes
    private void parseAttrStmt(int graphId) throws Exception {
        String token = currDotTokenizer.nextToken();

        if (token.equals(GRAPH)) {

        } else if (token.equals(NODE)) {

        } else if (token.equals(EDGE)) {

        } else {
            throw new Exception("[parseAttrStmt][line number = " + currDotTokenizer.getCurrentLineNumber() + "] Unknown symbol = " + token + ". Symbol expected = " + GRAPH + "|" + NODE + "|" + EDGE);
        }
        List<Integer> graphIds = new ArrayList<Integer>();
        graphIds.add(graphId);
        parseAttrList(FOR_GRAPH, graphIds);
    }

    private final static int FOR_GRAPH = 1;
    private final static int FOR_NODE = 2;
    private final static int FOR_EDGE = 3;

    private void parseAttrList(int forWhat, List<Integer> ids) throws Exception {
        String token = currDotTokenizer.nextToken();

        if (token.equals(BEGIN_ATTR)) {

        } else {
            throw new Exception("[parseAttrList][line number = " + currDotTokenizer.getCurrentLineNumber() + "] Unknown symbol = " + token + ". Symbol expected = " + BEGIN_ATTR);
        }

        parseAList(forWhat, ids);

        token = currDotTokenizer.nextToken();
        if (token == null || !token.equals(END_ATTR)) {
            throw new Exception("[parseAttrList][line number = " + currDotTokenizer.getCurrentLineNumber() + "] Unknown symbol = " + token + ". Symbol expected = " + END_ATTR);
        }

        token = currDotTokenizer.peekToken();
        if (token != null && token.equals(BEGIN_ATTR))
            parseAttrList(forWhat, ids);
    }

    private void parseAList(int forWhat, List<Integer> ids) throws Exception {
        parseAttr(forWhat, ids);

        String token = currDotTokenizer.peekToken();
        if (token != null && token.equals(COMMA)) {
            currDotTokenizer.nextToken();
            parseAList(forWhat, ids);
        }
    }

    private void parseAttr(int forWhat, List<Integer> ids) throws Exception {
        String id1 = currDotTokenizer.nextToken();
        String equal = currDotTokenizer.nextToken();
        String id2 = currDotTokenizer.nextToken();

        if (equal == null || !equal.equals(EQUAL)) {
            throw new Exception("[parseAttr][line number = " + currDotTokenizer.getCurrentLineNumber() + "] Unknown symbol = " + equal + ". Symbol expected = " + EQUAL);
        }

        if (id1 == null || id2 == null) {
            throw new Exception("[parseAttr][line number = " + currDotTokenizer.getCurrentLineNumber() + "] Parse attr failed. First or second ID = null.");
        }

        switch (forWhat) {
            case FOR_GRAPH: {
                // TODO Visual Graph hasn't graph attributes
                break;
            }

            case FOR_EDGE: {
                for (Integer id : ids) {
                    graphDataBase.createEdgeAttributeHeader(id, id1, id2, AttributeHeader.STRING_ATTRIBUTE_TYPE);
                }
                break;
            }

            case FOR_NODE: {
                for (Integer id : ids) {
                    graphDataBase.createVertexAttributeHeader(id, id1, id2, AttributeHeader.STRING_ATTRIBUTE_TYPE);
                }
                break;
            }

            default:
                break;
        }
    }

    /**
     * Returns vertex id which contains inner graph.
     */
    private int parseSubGraph(int graphId) throws Exception {
        int subGraphId = -1;

        if (currDotTokenizer.hasNextToken()) {
            String id = "";
            String token = currDotTokenizer.nextToken();

            if (token.equals(SUBGRAPH)) {
                token = currDotTokenizer.nextToken();
            }

            if (!token.equals(BEGIN_STMT)) {
                id = token;
                token = currDotTokenizer.nextToken();
            }

            if (token.equals(BEGIN_STMT)) {
                subGraphId = graphDataBase.createGraphHeader(graphModelId, currDirected, false);
                graphDataBase.createVertexHeader(graphId, subGraphId);

                currVertexDotID2databaseID = HashBiMap.create();
                currEdgeDotID2databaseID = HashBiMap.create();

                vertexDotID2databaseID.add(currVertexDotID2databaseID);
                edgeDotID2databaseID.add(currEdgeDotID2databaseID);

                parseStmtList(subGraphId);

                currVertexDotID2databaseID = vertexDotID2databaseID.pop();
                currEdgeDotID2databaseID = edgeDotID2databaseID.pop();
            }

            token = currDotTokenizer.nextToken();
            if (token == null || !token.equals(END_STMT)) {
                throw new Exception("[parseSubGraph][line number = " + currDotTokenizer.getCurrentLineNumber() + "] Unknown symbol = " + token + ". Symbol expected = " + END_STMT);
            }
        }

        return subGraphId;
    }

    private String parseID() throws Exception {
        String id = currDotTokenizer.nextToken();

        if (id == null || id.indexOf("{") >= 0 || id.indexOf("}") >= 0) {
            throw new Exception("[parseID][line number = " + currDotTokenizer.getCurrentLineNumber() + "] Unknown symbol = " + id + ". Symbol expected = ID");
        }

        return id;
    }

    private boolean isNextSubGraph() {
        String token = currDotTokenizer.peekToken();

        return token != null && (token.equals(SUBGRAPH) || token.equals(BEGIN_STMT));
    }

    private boolean isNextPort() {
        String token = currDotTokenizer.peekToken();

        return  token != null && token.equals(COLON);
    }

    private boolean isNextCompassPT() {
        String token = currDotTokenizer.peekToken();

        if (token == null)
            return false;

        if (token.equals(COLON))
            token = currDotTokenizer.peekToken(1);

        return token != null && (token.equals("n") || token.equals("ne") ||
                token.equals("e") || token.equals("se") ||
                token.equals("s") || token.equals("sw") ||
                token.equals("w") || token.equals("nw") ||
                token.equals("c") || token.equals("_"));
    }

    private boolean isNextGraph() {
        String token = currDotTokenizer.peekToken();

        if (token != null && token.equals(STRICT))
            token = currDotTokenizer.peekToken(1);

        return token != null && (token.equals(DIRECTED_GRAPH) || token.equals(UNDIRECTED_GRAPH));
    }

    private boolean isNextAttrStmt() {
        String token = currDotTokenizer.peekToken();

        return token != null && (token.equals(UNDIRECTED_GRAPH) || token.equals(NODE) || token.equals(EDGE));
    }

    private boolean isNextEdgeStmt() {
        for (int i = 0; i < 5; i++) {
            String token = currDotTokenizer.peekToken(i);

            if (token == null || token.equals(DELIMITER)) {
                return false;
            }

            if (token.equals(UNDIRECTED_EDGE) || token.equals(DIRECTED_EDGE)) {
                return true;
            }
        }

        return false;
    }

    private boolean isNextAttrList() {
        String token = currDotTokenizer.peekToken();

        return token != null && token.equals(BEGIN_ATTR);
    }

    private boolean isNextEdgeRHS() {
        String token = currDotTokenizer.peekToken();

        return token != null && (token.equals(DIRECTED_EDGE) || token.equals(UNDIRECTED_EDGE));
    }

    private boolean isNextID() {
        String token = currDotTokenizer.peekToken();

        return token != null && token.indexOf("{") < 0 && token.indexOf("}") < 0;
    }

//==============================================================================
//------------------PRIVATE CLASSES---------------------------------------------
    private static class DotTokenizer {
        private String prevToken = null;
        private StreamTokenizer tokenizer;
        private List<String> tokens;
        private List<Integer> lineNumbers;

        public DotTokenizer(Reader reader) {
            tokenizer = new StreamTokenizer(reader);
            tokenizer.slashSlashComments(true);
            tokenizer.slashStarComments(true);

            tokens = new ArrayList<String>();
            lineNumbers = new ArrayList<Integer>();
            readTokens(100);
        }

        public boolean hasNextToken() {
            if (tokens.size() == 0)
                readTokens(100);

            return tokens.size() != 0;
        }

        public String nextToken() {
            if (hasNextToken()) {
                String value = tokens.get(0);
                tokens.remove(0);
                lineNumbers.remove(0);
                return value;
            } else {
                return null;
            }
        }

        public String peekToken() {
            return peekToken(0);
        }

        public String peekToken(int number) {
            if (tokens.size() <= number) {
                readTokens(number - tokens.size() + 5);
                if (tokens.size() > number)
                    return tokens.get(number);
            } else {
                return tokens.get(number);
            }
            return null;
        }

        private void readTokens(int count) {
            while (count > 0) {
                try {
                    int tokenValueType;
                    String value = null;
                    if ((tokenValueType = tokenizer.nextToken()) != StreamTokenizer.TT_EOF) {
                        switch (tokenValueType) {
                            case StreamTokenizer.TT_WORD:
                                value = tokenizer.sval;
                                break;

                            case StreamTokenizer.TT_NUMBER:
                                value = Double.toString(tokenizer.nval);
                                break;

                            default:
                                if ((char)tokenValueType == '\"') {
                                    value = tokenizer.sval;
                                } else {
                                    value = Character.toString((char)tokenValueType);
                                }
                                break;
                        }
                    }

                    if (value != null) {
                        if (prevToken != null && prevToken.equals("-") && (value.equals(">") || value.equals("-"))) {
                            value = prevToken + value;
                            tokens.remove(tokens.size() - 1);
                            lineNumbers.remove(lineNumbers.size() - 1);
                        }

                        if (value.equals("-"))
                            count++;

                        tokens.add(value);
                        lineNumbers.add(tokenizer.lineno());

                        prevToken = value;
                    }
                } catch (Exception ex) {
                    VGMainServiceHelper.logger.printException(ex);
                }
                count--;
            }
        }

       private int getCurrentLineNumber() {
           return lineNumbers.size() == 0 ? -1 : lineNumbers.get(0);
       }
    }
}
